昨天我們已經把下載的股票清單存進 LiteDB,並且可以從本地載入。
今天要進一步加上 搜尋與篩選功能,讓使用者可以:
我們將 IStockRepository 與 IKdSignalService 結合,先把股票載入 ObservableCollection,再利用 ICollectionView 做即時篩選。
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;
using System.Threading.Tasks;
public class StockFilterViewModel : INotifyPropertyChanged
{
    private readonly IStockRepository _repo;
    private readonly IKdSignalService _kdService;
    public ObservableCollection<StockProfile> Stocks { get; } = new();
    public ICollectionView View { get; }
    private string _query;
    public string Query
    {
        get => _query;
        set { _query = value; OnPropertyChanged(nameof(Query)); View.Refresh(); }
    }
    private bool _onlyGoldenCross;
    public bool OnlyGoldenCross
    {
        get => _onlyGoldenCross;
        set { _onlyGoldenCross = value; OnPropertyChanged(nameof(OnlyGoldenCross)); View.Refresh(); }
    }
    public ICommand LoadFromLocalCommand { get; }
    public ICommand ClearFilterCommand { get; }
    public StockFilterViewModel(IStockRepository repo, IKdSignalService kdService)
    {
        _repo = repo ?? throw new ArgumentNullException(nameof(repo));
        _kdService = kdService ?? throw new ArgumentNullException(nameof(kdService));
        View = CollectionViewSource.GetDefaultView(Stocks);
        View.Filter = FilterPredicate;
        LoadFromLocalCommand = new RelayCommand(_ => LoadFromLocal());
        ClearFilterCommand = new RelayCommand(_ => { Query = string.Empty; OnlyGoldenCross = false; });
    }
    private void LoadFromLocal()
    {
        Stocks.Clear();
        foreach (var s in _repo.LoadStocks())
        {
            Stocks.Add(s);
        }
        _ = RefreshGoldenCrossCacheAsync(); // 異步計算 KD 訊號
        View.Refresh();
    }
    private bool FilterPredicate(object obj)
    {
        if (obj is not StockProfile s) return false;
        // 1) 關鍵字搜尋
        if (!string.IsNullOrWhiteSpace(Query))
        {
            var q = Query.Trim();
            if (!s.Code.Contains(q, StringComparison.OrdinalIgnoreCase) &&
                !s.Name.Contains(q, StringComparison.OrdinalIgnoreCase))
                return false;
        }
        // 2) KD 黃金交叉過濾
        if (OnlyGoldenCross)
        {
            if (!_goldenCache.TryGetValue(s.Code, out var isGolden) || !isGolden)
                return false;
        }
        return true;
    }
    // --- KD 快取 ---
    private readonly Dictionary<string, bool> _goldenCache = new();
    private async Task RefreshGoldenCrossCacheAsync()
    {
        _goldenCache.Clear();
        foreach (var s in Stocks)
        {
            bool isGolden = await _kdService.IsGoldenCrossAsync(s.Code);
            _goldenCache[s.Code] = isGolden;
        }
        View.Refresh();
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
我們沿用昨天的設計,IKdSignalService 從 日線資料計算 KD,判斷是否為黃金交叉。
(此處簡化為示範版本,正式版請接 API 或從本地資料庫讀日線 K 線來算。)
public interface IKdSignalService
{
    Task<bool> IsGoldenCrossAsync(string stockCode);
}
public class DemoKdSignalService : IKdSignalService
{
    public Task<bool> IsGoldenCrossAsync(string stockCode)
    {
        // 這裡應從 LiteDB 或 API 取近 9~20 日收盤價計算 KD
        // 範例:直接回傳 false,請在正式版替換
        return Task.FromResult(false);
    }
}
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="8" Spacing="8">
    <TextBox Width="200"
             Text="{Binding Query, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             ToolTip="輸入代號或名稱搜尋"/>
    <CheckBox Content="只顯示 KD 黃金交叉"
              IsChecked="{Binding OnlyGoldenCross, Mode=TwoWay}"/>
    <Button Content="載入本地資料"
            Command="{Binding LoadFromLocalCommand}" Width="120"/>
    <Button Content="清除篩選"
            Command="{Binding ClearFilterCommand}" Width="100"/>
</StackPanel>
<DataGrid ItemsSource="{Binding View}" Margin="8"
          AutoGenerateColumns="False" IsReadOnly="True">
    <DataGrid.Columns>
        <DataGridTextColumn Header="代號" Binding="{Binding Code}" Width="100"/>
        <DataGridTextColumn Header="名稱" Binding="{Binding Name}" Width="200"/>
        <DataGridTextColumn Header="產業" Binding="{Binding Industry}" Width="*"/>
        <DataGridTextColumn Header="更新時間"
                            Binding="{Binding LastUpdatedUtc, StringFormat=\{0:yyyy-MM-dd HH:mm\}}"
                            Width="150"/>
    </DataGrid.Columns>
</DataGrid>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        var repo = new LiteDbStockRepository("StockData.db");
        var kdService = new DemoKdSignalService(); // 正式版換成日線資料計算
        this.DataContext = new StockFilterViewModel(repo, kdService);
    }
}
今天我們完成了: